/*  This file is a part of JJSched source code
    JJSched - Java Jabber Scheduler - a simple Jabber scheduling application
    Copyright (C) 2008 Cestmir "CZestmyr" Houska

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/> */

package czestmyr.jjsched;

import java.lang.*;
import java.util.*;

import java.nio.charset.*;

import org.jivesoftware.smack.util.*;

enum Token {
	REGISTER, UNREGISTER, PING, ECHO, LIST, TIME, PLAN, OFFSET, SETOFFSET, GROUP,
	SUBSCRIBE, UNSUBSCRIBE, ADMIN, ME, USERS, ADMINS, MACROS, GROUPS, EVENTS, AT, DESCRIPTION,
	USER, ADD, NUMBER, MODIFY, NAME, SELF_SUBSCRIPTION, TRUE, FALSE, LISTUSERS,
	LISTEVENTS, PRIVILEGES, REMOVE, QUIT, HELP, INVALID;
}

/**
 * Parser of the commands written to the commandline or sent via XMPP.
 */
public class CommandParser {
	/**
	 * Constructor.
	 *
	 * Initializes the trie with all possible commands
	 */
	CommandParser(DatabaseConnector dbc, Config theConfig, Macros ms, TimeHandler time) {
		commands = new StringMatcher<Token>();

		commands.addMatch(Token.REGISTER, 	"register");
		commands.addMatch(Token.UNREGISTER, "unregister");
		commands.addMatch(Token.PING, 		"ping");
		commands.addMatch(Token.ECHO,		"echo");
		commands.addMatch(Token.LIST, 		"list");
		commands.addMatch(Token.TIME,		"time");
		commands.addMatch(Token.PLAN,		"plan");
		commands.addMatch(Token.OFFSET,		"offset");
		commands.addMatch(Token.SETOFFSET,	"setoffset");
		commands.addMatch(Token.GROUP,		"group");
		commands.addMatch(Token.SUBSCRIBE,	"subscribe");
		commands.addMatch(Token.UNSUBSCRIBE,"unsubscribe");
		commands.addMatch(Token.ADMIN,		"admin");
		commands.addMatch(Token.ME,			"me");
		commands.addMatch(Token.USERS,		"users");
		commands.addMatch(Token.ADMINS,		"admins");
		commands.addMatch(Token.MACROS,		"macros");
		commands.addMatch(Token.GROUPS,		"groups");
		commands.addMatch(Token.EVENTS,		"events");
		commands.addMatch(Token.AT,			"at");
		commands.addMatch(Token.DESCRIPTION,"description");
		commands.addMatch(Token.USER,		"user");
		commands.addMatch(Token.ADD,		"add");
		commands.addMatch(Token.NUMBER,		"number");
		commands.addMatch(Token.MODIFY,		"modify");
		commands.addMatch(Token.NAME,		"name");
		commands.addMatch(Token.SELF_SUBSCRIPTION,		"selfsubscription");
		commands.addMatch(Token.TRUE,		"true");
		commands.addMatch(Token.FALSE,		"false");
		commands.addMatch(Token.LISTUSERS,	"listusers");
		commands.addMatch(Token.LISTEVENTS, "listevents");
		commands.addMatch(Token.PRIVILEGES,	"privileges");
		commands.addMatch(Token.REMOVE,		"remove");
		commands.addMatch(Token.QUIT,		"quit");
		commands.addMatch(Token.HELP,		"help");

		theDBConnector = dbc;
		theMacros = ms;
		theTime = time;

        ucr = theConfig.getInt("UsersCanRegister").intValue() == 1;
	}

	/**
	 * Creates a new action that sends an error string to the user
	 */
	protected Action error(String errorString, String user) {
		Action ret = new Action(ActionId.ERROR);
		ret.sParam1 = errorString;
		ret.user = user;
		return ret;
	}

	/**
	 * Creates a new action that sends a message to a user
	 */
	protected Action message(String msgString, String user) {
		Action ret = new Action(ActionId.MESSAGE);
		ret.sParam1 = msgString;
		ret.user = user;
		return ret;
	}

	/**
	 * Parses the string and returns a token that represents it
	 */
	protected Token tokenize(String str) {
		Token ret = (Token)commands.match(str);
		if (ret == null) return Token.INVALID;
		else return ret;
	}

	/**
	 * Parses a list of string tokens and decides which action should be taken.
	 *
	 * This method is thread-safe. It had to be synchronized because some of its help values reside
	 * in the class itself as members.
	 *
	 * @param	tokens			List of strings to process
	 * @param	unescapedUser	Jabber id of the sending user as it originally came in the packet
	 * @param	user			Escaped jabber id (i.e. SQL injections safe)
	 */
	synchronized public Action parse(LinkedList<String> tokens, String unescapedUser, String user) {
		int tokensSize = tokens.size();

		if (tokensSize == 0) return error("No command provided", user);

        ////////////////////////////////////////
        //                                    //
        // Find out what the user wants to do //
        //                                    //
        ////////////////////////////////////////

		Action action = new Action(ActionId.NONE);
		action.user = user;

		// If the user is not registered, don't let the parsing continue (only for registration)
		if (!theDBConnector.isRegisteredUser(user)) {
			if (tokenize(tokens.get(0)) != Token.REGISTER) {
				return action;
			}
		}

		// Get the first token and build the corresponding action
		switch(tokenize(tokens.get(0))) {
			case HELP:
				//Generic help
            	if (tokens.size() == 1) {
                	action = message(Help.getBasicHelp(), user);
				//Command help
	            } else {
    	            action = message(Help.getTopicHelp(tokens.get(1)), user);
        	    }
			break;
			case PING:
				action = new Action(ActionId.ECHO);
				action.sParam1 = "A ping message just for you!";
			break;
			case ECHO:
            	StringBuilder sb = new StringBuilder("=> ");
            	ListIterator it2 = tokens.listIterator(1);
            	while (it2.hasNext()){
                	sb.append("\"");
                	sb.append((String)it2.next());
                	sb.append("\" ");
            	}
				action = new Action(ActionId.ECHO);
				action.sParam1 = sb.toString();
			break;
			case REGISTER:
        		if (tokensSize == 2) {
					token1 = tokenize(tokens.get(1));
            		//The user wants to register himself and the server allows it
            		if (token1 == Token.ME && ucr){
		                //We allow this only if the user is not registered yet
        		        if (!theDBConnector.isRegisteredUser(user)){
                		    action = new Action(ActionId.REGISTER);
					action.otherUser = user;
		                } else action = message("You are registered already.", user);
		            //The user wants to register someone else and is admin
        		    } else if (theDBConnector.adminCanModifyUsers(user)) {
						action = new Action(ActionId.REGISTER);
                		if (token1 == Token.ME) {
		                    action.otherUser = user;
        		        } else {
                		    action.otherUser = StringParser.escapeJID(tokens.get(1));
		                }
        		        //Check, whether the user in question is not registered
                		if (!theDBConnector.isRegisteredUser(action.otherUser)) {
							//Check validity of JID
		                	if ((StringUtils.parseName(action.otherUser) == "" ||
        			            StringUtils.parseServer(action.otherUser) == "") &&
								!action.otherUser.equals("console")) {
		                    	action = message(action.otherUser + " is not a valid JID.", user);
							}
        		        } else action = message("User " + action.otherUser + " is registered already.", user);
		            } else if (token1 == Token.ME) {
        		        action = message("This server does not allow ordinary users registering themselves.", user);
		            } else action = message("You don't have the privilege to register other people.", user);
        		} else action = error("Unknown command or bad syntax", user);
			break;
			case UNREGISTER:
        		if (tokensSize == 2){
					token1 = tokenize(tokens.get(1));
            		//The user wants to unregister himself
            		if (token1 == Token.ME){
                		//We allow this only if the user is not registered yet
                		if (theDBConnector.isRegisteredUser(user)){
                    		action = new Action(ActionId.UNREGISTER);
							action.otherUser = user;
                		} else action = message("You are not registered.", user);
            		//The user wants to unregister someone else and is admin
           	 		} else if (theDBConnector.adminCanModifyUsers(user)) {
                		action.otherUser = StringParser.escapeJID(tokens.get(1));
                		//Check that the user in question is registered
                		if (theDBConnector.isRegisteredUser(action.otherUser)) {
                    		action.id = ActionId.UNREGISTER;
                		} else action = message("User " + action.otherUser + " is not registered.", user);
          	  		} else action = message("You don't have the privilege to unregister other people.", user);
        		} else action = error("Unknown command or bad syntax", user);
			break;
			case LIST:
        		if (tokensSize == 2){
					token1 = tokenize(tokens.get(1));
					switch (token1) {
					case USERS:
		                if (theDBConnector.adminCanModifyUsers(user)){
							action = new Action(ActionId.LIST_USERS);
               			} else action = message("You don't have the privilege to list users", user);
					break;
					case ADMINS:
               			if (theDBConnector.adminCanModifyAdmins(user)){
							action.id = ActionId.LIST_ADMINS;
               			} else action = message("You don't have the privilege to list admins", user);
					break;
					case MACROS:
		                Iterator macroit = theMacros.getMacrosIterator();
						action.id = ActionId.MESSAGE;
           			    action.sParam1 = "Currently defined macros and their substitutions:\n";
		                while (macroit.hasNext()){
           			        Map.Entry<String, Vector<String>> macro = (Map.Entry<String, Vector<String>>)macroit.next();
			                action.sParam1 += "\"" + macro.getKey() + "\" => ";
               			    Iterator substit = ((Vector<String>)macro.getValue()).iterator();
		                    while (substit.hasNext()){
           		            	action.sParam1 += "\"" + substit.next() + "\" ";
                   			}
	                    	action.sParam1 += "\n";
						}
       		      	break;
					case GROUPS:
		                //Check that the user can modify groups
               			if (theDBConnector.adminCanModifyGroups(user)){
							action.id = ActionId.LIST_GROUPS;
           			    } else {
		                    action = message("You don't have the privilege to list groups", user);
           			    }
					break;
					case EVENTS:
						action.id = ActionId.LIST_EVENTS;
						action.user = user;
					break;
					default:
						action = message("Invalid list entity. See 'help list' for valid list entities.", user);
					break;
					}
			    } else action = error("Unknown command or bad syntax", user);
			break;
			case TIME:
				if (tokensSize >= 1){
		            //A user has requested to view the server time
            		if (tokensSize == 1) {
                		action = message(theTime.getFormattedTime(), user);
            		} else if (tokensSize == 2){
						token1 = tokenize(tokens.get(1));
                		//A user has requested to show his time
						if (token1 == Token.ME){
                    		action = message(theTime.getFormattedTime(theTime.getLongTime()+theDBConnector.getUserTimeOffset(user)), user);
	                	//An admin has requested to show a time of another user
    	            	} else if (theDBConnector.adminCanModifyUsers(user)){
        	            	action.otherUser = StringParser.escapeJID(tokens.get(1));
            	        	if (theDBConnector.isRegisteredUser(action.otherUser)){
                	        	action = message(theTime.getFormattedTime(theTime.getLongTime() 
                    	        	             		                + theDBConnector.getUserTimeOffset(action.otherUser)), user);
                    		} else action = message("User "+tokens.get(1)+" is not a registered user", user);
                		} else action = message("You don't have the rights to view other user's information", user);
            		} else action = message("Invalid number of arguments for 'time'. See 'help time' for more info.", user);
				} else action = error("Unknown command or bad syntax", user);
			break;
			case PLAN:
			if (tokensSize >= 1) {
	            boolean commandIsExpected = true;
    	        boolean validPlan = true;

	            //Prepare the plan struct
	            action.plan.singleuser = true;
	            action.plan.timestamp = 0;
	            action.plan.username = null;
	            action.plan.description = null;

	            PlanCommand cmd = PlanCommand.NONE;
	            for (int i = 1; i < tokens.size(); ++i){
                if (commandIsExpected) {
					token1 = tokenize(tokens.get(i));
                    if (token1 == Token.AT){
                        cmd = PlanCommand.AT;
                    } else if (token1 == Token.DESCRIPTION) {
                        cmd = PlanCommand.DESCRIPTION;
                    } else if (token1 == Token.USER) {
                        cmd = PlanCommand.USER;
                    } else if (token1 == Token.GROUP){
                        cmd = PlanCommand.GROUP;
                    } else {
                        action = message("Invalid parameter for command plan: "+tokens.get(i), user);
                        validPlan = false;
                        break;
                    }
                } else {
                    switch(cmd) {
                        case AT:
                            try {
                                action.plan.timestamp = theTime.parseToServerTime(tokens.get(i), 
                                                                           theDBConnector.getUserTimeOffset(user));
                            } catch (Exception e) {
                                action = message("Time expression parse error: "+e.getMessage(), user);
                                //e.printStackTrace();
                                validPlan = false;
                            }
                        break;
                        case DESCRIPTION:
                            action.plan.description = tokens.get(i);
                        break;
                        case USER:
                            action.plan.singleuser = true;
                            if (tokenize(tokens.get(i)) == Token.ME){
                                action.plan.username = user;
                            //You can plan for other users only if you have the privilege to modify users
                            } else if (theDBConnector.adminCanModifyUsers(user)) {
                                action.plan.username = StringParser.escapeJID(tokens.get(i));
                            } else {
                                action = message("You cannot plan events for other users, unless you are admin with the"
                                        	   + " privilege to modify users.", user);
                                validPlan = false;
                            }
                        break;
                        case GROUP:
                            //You can plan events for groups only if you have the privilege to modify groups
                            //Or if you are administrator of the given group
                            action.plan.singleuser = false;
                            try {
                                if (theDBConnector.adminCanModifyGroups(user) ||
                                    theDBConnector.userCanPlanGroup(user, theDBConnector.getGroupID(action.plan.username))) {
                                    if (tokenize(tokens.get(i)) == Token.NUMBER) {
                                        ++i;
                                        action.plan.groupId = Integer.parseInt(tokens.get(i));
                                    } else {
                                        action.plan.groupId = 0;
                                    }
                                } else {
                                    action = message("You cannot plan events for groups, unless you are admin with the"
                                            	   + " privilege to modify groups or have the privileges to plan for the given group.", user);
                                    validPlan = false;
                                }
                            } catch (Exception e) {
                                action = message("Could not plan group event - DB error: "+e.getMessage(), user);
								validPlan = false;
                            }
                        break;
                    }
                }
                commandIsExpected = !commandIsExpected;
            	}
            	if (action.plan.timestamp == 0 && validPlan) {
               		action = message("Missing time specifier in plan command", user);
                	validPlan = false;
	            }
    	        if (action.plan.username == null) {
        	        action.plan.username = user;
	            }
    	        if (action.plan.description == null) {
        	        action.plan.description = "You have a plan for now without a description!";
	            }
    	        if (validPlan) action.id = ActionId.PLAN;
			} else action = error("Unknown command or bad syntax", user);
        	break;
			case OFFSET:
		        if (tokensSize >= 1) {
		            //A user is requesting his own offset
        		    if (tokensSize == 1) {
                		action = message(theTime.getFormattedOffset(theDBConnector.getUserTimeOffset(user)), user);
        		    } else if (tokensSize == 2) {
						token1 = tokenize(tokens.get(1));
                		//A user has requested to show his offset
                		if (token1 == Token.ME){
                    		action = message(theTime.getFormattedOffset(theDBConnector.getUserTimeOffset(user)), user);
                		}
                		//An admin is requesting someone else's offset
                		else if (theDBConnector.adminCanModifyUsers(user)){
                    		action.otherUser = StringParser.escapeJID(tokens.get(1));
                    		if (theDBConnector.isRegisteredUser(action.otherUser)){
                        		action = message(theTime.getFormattedOffset(theDBConnector.getUserTimeOffset(action.otherUser)), user);
		                    } else action = message("User "+tokens.get(1)+" is not a registered user", user);
		                } else action = message("You don't have the privilege to modify or view other users' information.", user);
		            } else action = message("Wrong argument count for 'offset', please refer to 'help offset' for more info.", user);
		        } else action = error("Unknown command or bad syntax", user);
			break;
			case SETOFFSET:
        		if (tokensSize == 3) {		
		            boolean cont = true;
					token1 = tokenize(tokens.get(1));

            		//A user sets his own offset
            		if (token1 == Token.ME) {
		                action.otherUser = user;
            		//An admin sets somebody else's offset
            		} else if (theDBConnector.adminCanModifyUsers(user)) {
                		action.otherUser = StringParser.escapeJID(tokens.get(1));
                		if (!theDBConnector.isRegisteredUser(action.otherUser)) {
                    		action = message("The user "+tokens.get(1)+" is not registered", user);
                    		cont = false;
                		}
		            //A user tries to set someone else's offset
            		} else {
                		action = message("You don't have the privilege to modify users", user);
                		cont = false;
            		}
            		//Try to decode the timediff expression
            		if (cont) try {
		                String td = tokens.get(2);
		                int m = td.indexOf('-');
		                int p = td.indexOf('+');
		                if (p != -1 && m != -1) {
		                    action = message("You cannot have both '-' and '+' before the timediff expression", user);
		                    cont = false;
		                } else if (p != -1) {
		                    action.lParam = (int)theTime.parseTimeDiff(td.substring(p+1));
		                } else if (m != -1) {
		                    action.lParam = -(int)theTime.parseTimeDiff(td.substring(m+1));
		                } else {
		                    action.lParam = (int)theTime.parseTimeDiff(td);
		                }
		            } catch (Exception e) {
		                action = message(e.getMessage(), user);
		                cont = false;
		            }
		            if (cont) {
						action.id = ActionId.SETOFFSET;
						action.user = user;
					}
		        } else action = error("Unknown command or bad syntax", user);
			break;
			case GROUP:
	        	if (tokensSize >= 2) {
					token1 = tokenize(tokens.get(1));
		            //Adding new groups is only for global group admins
        		    if (token1 == Token.ADD) {
                		if (tokensSize != 4) {
		                    action = message("Incorect format of group command. See 'help group' for the correct syntax.", user);
        		        } else {
                    		if (!theDBConnector.adminCanModifyGroups(user)){
                        		action = message("You don't have the privilege to add new groups", user);
		                    } else {
								action.id = ActionId.GROUP_ADD;
								action.sParam1 = tokens.get(2);
								action.sParam2 = tokens.get(3);
        		            }
                		}
		            } else {
		            //Variants 2-7 are for global admins or for the specified group admins
	            	    //Get group identification in case we will need it
	        	        //Try block is necessary here in case number conversion or DB access fails
    		            try {
							boolean groupByName = false;
                    		if (tokensSize >= 3) {
                        		if (tokenize(tokens.get(2)) == Token.NUMBER) {
                            		if (tokensSize == 3) {
                                		action = message("Incorrect group specification. See 'help groupspec' for details.", user);
		                            } else {
		                                groupByName = false; //groupID is in tokens.get(3)
        		                        action.lParam = Integer.parseInt(tokens.get(3));
                		            }
		                        } else {
		                            groupByName = true; //Name is always in tokens.get(2)
        		                    action.lParam = theDBConnector.getGroupID(tokens.get(2));
                		        }
		                    } else {
								action = message("Bad group command syntax", user);
								break;
							}

							//Check permissions
							if (token1 == Token.REMOVE) {
								if (!theDBConnector.adminCanModifyGroups(user)) {
									action = message("You don't have the privilege to modify groups.", user);
									break;
								}
							} else if (token1 == Token.MODIFY ||
								token1 == Token.SUBSCRIBE ||
								token1 == Token.UNSUBSCRIBE ||
								token1 == Token.PRIVILEGES) {
								if (!theDBConnector.adminCanModifyGroups(user) &&
									!theDBConnector.userCanModifyGroup(user, action.lParam)) {
									action = message("You don't have the privilege to modify this group"+
													" (and you cannot modify groups in general).", user);
									break;
								}
							} else if (token1 == Token.LISTUSERS ||
								token1 == Token.LISTEVENTS) {
								if (!theDBConnector.adminCanModifyGroups(user) &&
									!theDBConnector.userCanListGroup(user, action.lParam)) {
									action = message("You don't have the privilege to list this group"+
													" (and you cannot modify groups in general).", user);
									break;
								}
							}

		                        int i = 3; //Used to get the correct token
        		                if (!groupByName) ++i;
                		        if (token1 == Token.MODIFY) {
                            		while (i < tokensSize) {
										token2 = tokenize(tokens.get(i));
                                		if (token2 == Token.DESCRIPTION) {
		                                    ++i;
        		                            if (i < tokensSize)
                		                        action.sParam1 = tokens.get(i);
		                                    else {
        		                                action = message("No new group description specified in the group command.", user);
                		                    }
		                                } else if (token2 == Token.NAME) {
		                                    ++i;
        		                            if (i < tokensSize)
                		                        action.sParam2 = tokens.get(i);
                        		            else {
                                		        action = message("No new group name specified in the group command.", user);
		                                    }
		                                } else if (token2 == Token.SELF_SUBSCRIPTION) {
		                                    ++i;
        		                            if (i < tokensSize) {
												token2 = tokenize(tokens.get(i));
                		                        if (token2 == Token.TRUE)
                        		                    action.iParam = 1;
                                		        else if (token2 == Token.FALSE)
                                        		    action.iParam = 0;
		                                        else {
        		                                    action = message("Missing boolean value after \"selfsubscription\".", user);
                		                            i = tokensSize; //Break from while statement
                        		                }
											} else {
		                                        action = message("Missing \"true\" or \"false\" after \"selfsubscription\".", user);
        		                            }
                		                } else {
		                                    action = message("Incorrect format of group command. See 'help group' for the correct syntax.", user);
		                                    i = tokensSize; //break from the while statement and *hack warning* serve as flag for incorrect parsing
        		                        }
                		                ++i;
                            		}
		                            if (i == tokensSize) //i serves as a flag for correct group statement here
        		                        action.id = ActionId.GROUP_MODIFY;
		                        } else if (token1 == Token.LISTUSERS) {
		                            if (tokensSize != i) // i should equal the correct number of tokens in this command - 3 or 4
        		                        action = message("Incorrect format of group command. See 'help group' for the correct syntax.", user);
                		            else
                        		        action.id = ActionId.GROUP_LISTUSERS;
		                        } else if (token1 == Token.SUBSCRIBE) {
        		                    if (tokensSize != i+1) // i+1 should equal the correct number of tokens in this command - 4 or 5
                		                action = message("Incorrect format of group command. See 'help group' for the correct syntax.", user);
                        		    else {
                                		action.otherUser = StringParser.escapeJID(tokens.get(i));
		                                action.id = ActionId.GROUP_SUBSCRIBE;
        		                    } //--------------------------------------------------------
                		        } else if (token1 == Token.UNSUBSCRIBE) {
		                            if (tokensSize != i+1)
        		                        action = message("Incorrect format of group command. See 'help group' for the correct syntax.", user);
		                            else {
        		                        action.otherUser = StringParser.escapeJID(tokens.get(i));
                		                action.id = ActionId.GROUP_UNSUBSCRIBE;
                        		    }
		                        } else if (token1 == Token.PRIVILEGES) {
		                            if (tokensSize != i+2)
        		                        action = message("Incorrect format of group command. See 'help group' for the correct syntax.", user);
		                            else {
        		                        action.otherUser = StringParser.escapeJID(tokens.get(i));
                		                action.iParam = Integer.parseInt(tokens.get(i+1));
										action.id = ActionId.GROUP_PRIVILEGES;
		                            }
        		                } else if (token1 == Token.REMOVE) {
		                            if (tokensSize != i)
        		                        action = message("Incorrect format of group command. See 'help group' for the correct syntax.", user);
                		            else {
										action.id = ActionId.GROUP_REMOVE;
		                            }
								} else if (token1 == Token.LISTEVENTS) {
									if (tokensSize != i)	
        		                        action = message("Incorrect format of group command. See 'help group' for the correct syntax.", user);
                		            else {
										action.id = ActionId.GROUP_LISTEVENTS;
		                            }
        		                } else {
		                            action = message("Unrecognized group command. See 'help group' for more details", user);
		                        }
        		        } catch (Exception e) {
                		    action = message(e.getMessage(), user);
		                }
		            }
        		} else action = error("Unknown command or bad syntax", user);
			break;
			case SUBSCRIBE:
				if (tokensSize == 2) {
		            //Check if ordinary users can subscribe to group
        		    try {
	    	            //Get group ID
	        	        action.lParam = theDBConnector.getGroupID(tokens.get(1));
    	        	    //Check if users can subscribe to the group
	                	if (theDBConnector.usersCanSubscribe(action.lParam)) {
        	            	action.otherUser = action.user = user;
							action.id = ActionId.GROUP_SUBSCRIBE;
    	            	} else action = message("Sorry, ordinary users cannot subscribe to this group. "+
												"Contact group admin for subscription.", user);
		            } catch (Exception e) {
        		        action = message(e.getMessage(), user);
		            }
        		} else action = error("Unknown command or bad syntax", user);
			break;
			case UNSUBSCRIBE:
        		if (tokensSize == 2) {
		            //Check if ordinary users can subscribe to group
        			try {
		                //Get group ID
        		        action.lParam = theDBConnector.getGroupID(tokens.get(1));
						action.id = ActionId.GROUP_UNSUBSCRIBE;
        		        action.otherUser = action.user = user;
		            } catch (Exception e) {
        		        action = message(e.getMessage(), user);
		            }
        		} else action = error("Unknown command or bad syntax", user);
			break;
			case ADMIN:
        		if (tokensSize == 3) {
		            //Get the other user's jabber ID
        		    action.otherUser = StringParser.escapeJID(tokens.get(1));
		            //Check, whether the sending user has the privileges to modify admins
		            if (!theDBConnector.adminCanModifyAdmins(user))
		                action = message("You don't have the privileges to modify admins", user);
		            else try {
        		        action.iParam = Integer.parseInt(tokens.get(2));
						action.user = user;
             		    //Disallow removing privilege to modify admins from self (ensure at least one admin)
		                if (action.iParam%2 == 0 && user.equals(action.otherUser))
        		            throw new Exception("Cannot remove admin privileges from self");
						action.id = ActionId.GROUP_ADMIN;
        		    } catch (Exception e) {
                		action = message(e.getMessage(), user);
		            }
        		} else action = error("Unknown command or bad syntax", user);
			break;

			//Finally, the possibility to quit jjsched from console and default action - error message
			case QUIT:
				if (unescapedUser.equals("console")) {
					action = new Action(ActionId.QUIT);
					break;
				}
			default:
				action = error("Unknown command or bad syntax", user);
			break;
		}

		action.unescapedUser = unescapedUser;
		action.user = user;

		return action;
	}

	Token token1;
	Token token2;

	private StringMatcher<Token> commands;
	private boolean ucr;

	private DatabaseConnector theDBConnector;
	private Macros theMacros;
	private TimeHandler theTime;
}

/** States of 'plan' command parser */
enum PlanCommand{
    NONE, AT, DESCRIPTION, USER, GROUP
}
